package org.eclipse.gittab.player.internal.midi;

import static org.eclipse.gittab.player.internal.midi.MidiUtil.createInstrumentMessage;

import java.util.List;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.Sequence;
import javax.sound.midi.Track;

import org.eclipse.gittab.gitTabDsl.Tuning;
import org.eclipse.gittab.player.ComposedTab;
import org.eclipse.gittab.player.Composer;
import org.eclipse.gittab.player.PlayableNote;
import org.eclipse.gittab.player.Player;

/**
 * Class responsible for creating MIDI {@link Sequence}s. To be able to
 * reconstruct which string has been played, each string is played on a
 * different MIDI channel, numbered from channel 0 to number of strings
 * specified in the {@link Tuning}. </p>This sequences can be played in a
 * {@link Player}.
 * 
 * @author mtreitle
 * 
 */
public class MidiSequenceComposer implements Composer {

	private final static int PPQS = 16; // you can play 64th notes
	private final static int STACCATO = 8; // staccato note

	private Sequence sequence;
	private Track track;
	private long currentTick; // current time-position in PPQS

	private final int totalStrings;

	public MidiSequenceComposer(int totalStrings) {
		reset();
		this.totalStrings = totalStrings;
	}

	/**
	 * Resets the sequence and removes all MIDI events from it.
	 */
	public void reset() {

		try {
			sequence = new Sequence(Sequence.PPQ, PPQS);
		} catch (InvalidMidiDataException e) {
			e.printStackTrace();
		}
		track = sequence.createTrack();
		currentTick = 0;
	}

	@Override
	public void pause(int noteValue) {

		if (!checkNoteValue(noteValue))
			return;

		currentTick += calcTicks(noteValue);
	}

	@Override
	public void chord(List<PlayableNote> notes, int noteValue, int velocity,
			int instrument) {

		if (!checkNoteValue(noteValue))
			return;

		changeInstrument(instrument);
		generateChord(notes, noteValue, velocity, false);
	}

	/**
	 * Automatically calculates the picking speed from the note value.
	 */
	@Override
	public void chordPicked(List<PlayableNote> notes, int noteValue,
			int velocity, PickingMode mode, int instrument) {

		if (!checkNoteValue(noteValue))
			return;

		// max number of ticks for this chord
		int max = calcTicks(noteValue);

		int nrNotes = notes.size();
		if (nrNotes % 2 == 0) {
			nrNotes += 0; // +2 makes the picking faster
		} else {
			nrNotes += 1; // make it even
		}

		// note ticks per single note in this chord
		int noteTicks = max / nrNotes;

		changeInstrument(instrument);
		for (int i = 0; i < notes.size(); i++) {

			int idx = i;
			if (mode == PickingMode.UP) { // invert used notes
				idx = notes.size() - i - 1;
			}

			generateToneManually(notes.get(idx), currentTick,
					currentTick + max, velocity);
			currentTick += noteTicks;
		}
	}

	/**
	 * Single note.
	 * 
	 * @param noteValue
	 *            either 1, 2, 4, 8, 16, 32, 64
	 */
	@Override
	public void note(PlayableNote note, int noteValue, int velocity,
			int instrument) {

		if (!checkNoteValue(noteValue))
			return;

		changeInstrument(instrument);
		generateTone(note, noteValue, velocity, false);
	}

	/**
	 * Number of the last instrument being played.
	 */
	private int lastInstrument;

	/**
	 * Changes the instrument on all channels if the instrument has changed.
	 * 
	 * @param instrument
	 */
	private void changeInstrument(int instrument) {

		if (instrument != lastInstrument) {

			for (int channel = 0; channel < totalStrings; channel++) {

				track.add(new MidiEvent(createInstrumentMessage(instrument,
						channel), currentTick));
			}
			lastInstrument = instrument;
		}
	}

	/**
	 * Adds a note to the main track not calculating the ticks by the note
	 * value.
	 * 
	 * @param note
	 * @param ticks
	 * @param velocity
	 */
	private void generateToneManually(PlayableNote note, long startTick,
			long endTick, int velocity) {

		track.add(new MidiEvent(MidiUtil.createOnMessage(note.getNote(), note
				.getString(), velocity), startTick));

		track.add(new MidiEvent(MidiUtil.createOffMessage(note.getNote(), note
				.getString()), endTick));
	}

	/**
	 * Adds a note to the main track.
	 * 
	 * @param note
	 * @param noteValue
	 *            must not be more than 64
	 * @param velocity
	 * @param staccato
	 */
	private void generateTone(PlayableNote note, int noteValue, int velocity,
			boolean staccato) {

		track.add(new MidiEvent(MidiUtil.createOnMessage(note.getNote(), note
				.getString(), velocity), currentTick));
		currentTick += calcTicks(noteValue);
		if (staccato) {
			currentTick -= STACCATO;
		}

		track.add(new MidiEvent(MidiUtil.createOffMessage(note.getNote(), note
				.getString()), currentTick));
		if (staccato) {
			currentTick += STACCATO;
		}
	}

	/**
	 * Adds several notes to the main track.
	 * 
	 * @param notes
	 * @param noteValue
	 *            must not be more than 64
	 * @param velocity
	 * @param staccato
	 */
	private void generateChord(List<PlayableNote> notes, int noteValue,
			int velocity, boolean staccato) {

		for (PlayableNote note : notes) {
			track.add(new MidiEvent(MidiUtil.createOnMessage(note.getNote(),
					note.getString(), velocity), currentTick));
		}
		currentTick += calcTicks(noteValue);
		if (staccato) {
			currentTick -= STACCATO;
		}
		for (PlayableNote note : notes) {
			track.add(new MidiEvent(MidiUtil.createOffMessage(note.getNote(),
					note.getString()), currentTick));
		}
		if (staccato) {
			currentTick += STACCATO;
		}
	}

	/**
	 * Checks if the note value can be processed.
	 * 
	 * @param noteValue
	 * @return
	 */
	private final boolean checkNoteValue(int noteValue) {
		return noteValue <= PPQS * 4; // max 64th can be played
	}

	/**
	 * Calculates the ticks for this note value.
	 * 
	 * @param noteValue
	 * @return
	 */
	protected int calcTicks(int noteValue) {
		return (int) (PPQS * ((double) 4 / noteValue));
	}

	/**
	 * Gets the current {@link Sequence}.
	 * 
	 * @return
	 */
	public Sequence getSequence() {
		return sequence;
	}

	@Override
	public ComposedTab getComposedTab() {

		return new ComposedTab() {
			@Override
			public Object getTab() {
				return getSequence();
			}
		};
	}
}
